﻿/*
 * Copyright 2011 Edward L. Stauff.                     Contact: <EdStauff@gmail.com>.
 * 
 * This file, "CmdLineParamGroup.cs", is part of the "Stauffware.Common.Console" class 
 * library.  "Stauffware.Common" is free software: you can redistribute it and/or 
 * modify it under the terms of the GNU Lesser Public License as published by the 
 * Free Software Foundation, either version 3 of the License, or (at your option) 
 * any later version.
 * 
 * "Stauffware.Common" is distributed in the hope that it will be useful, 
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser Public License for more 
 * details.
 * 
 * You should have received a copy of the GNU Lesser Public License along with 
 * "Stauffware.Common".  If not, see <http://www.gnu.org/licenses/>.
 */

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Reflection;
using System.Resources;
using System.Text;
using Stauffware.Common.Core;

namespace Stauffware.Common.Console
{
	/// <summary>
	/// A group of command-line params that were defined in a single class.
	/// </summary>
	public class CmdLineParamGroup
	{
		/// <summary>
		/// Gets the object that will receive the parameter values.
		/// </summary>
		public readonly object ParamsObject;
		
		/// <summary>
		/// Gets the name of the parameter group, or null if it has no name.
		/// </summary>
		public string GroupName { get; private set; }
		
		/// <summary>
		/// Help for the group, or the name of a resource containing help.
		/// </summary>
		public string GroupHelp { get; private set; }

		/// <summary>
		/// Gets the CmdLineParser to which this CmdLineParamGroup belongs.
		/// </summary>
		public readonly CmdLineParser CmdLineParser;

		// If the group has a name, this contains all the group's parameters.
		// If the group does not have a name, this is null and the group's
		// parameters are handled directly by CmdLineParser.
		internal readonly PrefixDictionary<CmdLineParam> AllParams;

		/// Specifies whether to automatically create command-line parameters 
		/// for public fields that are not tagged with CmdLineArgAttribute or 
		/// NotAvailableFromCmdLineAttribute. 
		private bool IncludePublicFieldsByDefault = true;

		/// Specifies whether to automatically create command-line parameters
		/// for public properties that are not tagged with CmdLineArgAttribute 
		/// or NotAvailableFromCmdLineAttribute. 
		private bool IncludePublicPropertiesByDefault = true;

		// shorcut for this.ParamsObject.GetType()
		private Type ParamsObjType { get { return this.ParamsObject.GetType(); } }

		// for collecting multiple errors to report all at once
		private List<string> Errors = new List<string>();

		//-----------------------------------------------------------------------------
		/// <summary>
		/// constructor for a "dummy" group
		/// </summary>

		internal CmdLineParamGroup (CmdLineParser parser, string groupName=null)
		{
			Require.NotNull(parser);
			this.CmdLineParser = parser;
			this.GroupName = groupName;
			if (!string.IsNullOrEmpty(this.GroupName))
				this.AllParams = new PrefixDictionary<CmdLineParam>(true);
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// constructor
		/// </summary>
		/// <param name="clParser">the CmdLineParser object to which this group belongs</param>
		/// <param name="paramsObj">
		///		the object whose fields and parameters contain the values of the command-line params
		/// </param>
		/// <param name="groupAttr">
		///		if non-null, these attributes will be used instead of any that might be attached to the paramsObj class
		/// </param>
		/// <param name="memberAttrs">
		///		If non-null, maps field and parameter names to CmdLineArgAttribute objects.
		/// </param>
		/// <param name="positionalParams">
		///		Output: any positional parameters that are created will be added to this list.
		/// </param>

		internal CmdLineParamGroup (CmdLineParser clParser, object paramsObj, CmdLineGroupAttribute groupAttr,
						IDictionary<string, CmdLineArgAttribute> memberAttrs, List<CmdLineParam> positionalParams)
		{
			Require.NotNull(clParser);
			Require.NotNull(paramsObj);

			this.CmdLineParser = clParser;
			this.ParamsObject = paramsObj;

#if !SILVERLIGHT
			ApplicationSettingsBase settingsObj = this.ParamsObject as ApplicationSettingsBase;
#endif
			if (groupAttr == null)
				groupAttr = this.ParamsObjType.GetCustomAttribute<CmdLineGroupAttribute>(true);

			//if (settingsObj == null && this.ClpObj == null && groupAttr == null)
			//    throw new Exception("a command-line params object type must " +
			//                        "(a) derive from ApplicationSettingsBase, " + 
			//                        "(b) implement ICmdLineParams, or " + 
			//                        "(c) have a CmdLineGroupAttribute");

			this.GetStuffFrom(groupAttr);
#if !SILVERLIGHT
			this.GetStuffFromSettingsObj(settingsObj);
#endif
			if (!string.IsNullOrEmpty(this.GroupName))
				this.AllParams = new PrefixDictionary<CmdLineParam>(true);

			foreach (CmdLineValueParserAttribute vpa in this.ParamsObjType.GetCustomAttributes(
													typeof(CmdLineValueParserAttribute), false))
			{
				this.CmdLineParser.ValueParserFactory.AddCustom(paramsObj, vpa.MethodName);
			}

			// create all the individual CmdLineParam objects

			BindingFlags memberBindingFlags = BindingFlags.Public | BindingFlags.Instance 
																  | BindingFlags.DeclaredOnly;

			foreach (PropertyInfo pi in this.ParamsObjType.GetProperties(memberBindingFlags))
				this.Add(this.CreateParam(pi, LookupAttr(memberAttrs, pi.Name)), positionalParams);

			foreach (FieldInfo fi in this.ParamsObjType.GetFields(memberBindingFlags))
				this.Add(this.CreateParam(fi, LookupAttr(memberAttrs, fi.Name)), positionalParams);

			if (this.Errors.Count > 0)
			{
				ListString list = new ListString("\n");
				foreach (string err in this.Errors)
					list.Add(err);
				throw new Exception("Error(s) in command-line parameter definitions:\n" + list.ToString());
			}
		}

		//-----------------------------------------------------------------------------

		internal CmdLineArgAttribute LookupAttr (IDictionary<string, CmdLineArgAttribute> dict, string name)
		{
			if (dict == null) return null;
			CmdLineArgAttribute attr = null;
			dict.TryGetValue(name, out attr);
			return attr;
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Looks up the given parameter or switch name.
		/// </summary>

		internal PrefixMatch LookupParam (string argName, out CmdLineParam param)
		{
			return this.AllParams.Lookup(argName, out param);
		}

		//-----------------------------------------------------------------------------

		private void GetStuffFrom (CmdLineGroupAttribute clpAttr)
		{
			if (clpAttr == null)
				return;

			this.GroupName = clpAttr.GroupName;
			this.GroupHelp = clpAttr.GroupHelp;
			this.IncludePublicFieldsByDefault = clpAttr.IncludePublicFieldsByDefault;
			this.IncludePublicPropertiesByDefault = clpAttr.IncludePublicPropertiesByDefault;

			//if (this.ClpAttr.ResourceNamePrefix != null)
			//    this.GroupHelp = ResourceManager.GetString(this.ClpAttr.ResourceNamePrefix + this.ClpAttr.GroupHelp);
		}

		//-----------------------------------------------------------------------------

#if !SILVERLIGHT
		private void GetStuffFromSettingsObj (ApplicationSettingsBase settingsObj)
		{
			if (string.IsNullOrEmpty(this.GroupName))
			{
				SettingsGroupNameAttribute nameAttr = 
					this.ParamsObjType.GetCustomAttribute<SettingsGroupNameAttribute>(false);
				if (nameAttr != null)
					this.GroupName = nameAttr.GroupName;
			}

			if (string.IsNullOrEmpty(this.GroupHelp))
			{
				SettingsGroupDescriptionAttribute descAttr = 
					this.ParamsObjType.GetCustomAttribute<SettingsGroupDescriptionAttribute>(false);
				if (descAttr != null)
					this.GroupHelp = descAttr.Description;
			}

			if (settingsObj == null)
				return;

			// QUESTION: should we care about SettingsObj.SettingsKey?

		}
#endif
		//-----------------------------------------------------------------------------
		/// <summary>
		/// Returns a parser delegate that can be used with the given type, or null if
		/// one couldn't be found.
		/// </summary>

		internal ICmdLineValueParser GetValueParser (Type valueType)
		{
			return this.CmdLineParser.GetValueParser(valueType);
		}

		//-----------------------------------------------------------------------------
		// Creates a command-line parameter object that puts its value into a property.
		// Returns null if a parameter wasn't created, either because of an error 
		// (which will have been added to this.Errors, or because no parameter needs
		// to be created for the given property.

		private CmdLineParam CreateParam (PropertyInfo pi, CmdLineArgAttribute clpAttr)
		{
			if (clpAttr == null)
				clpAttr = pi.GetCustomAttribute<CmdLineArgAttribute>(false);

			if (clpAttr == null)
			{
				if (pi.GetGetMethod() == null || pi.GetSetMethod() == null)
					return null;
				if (!this.IncludePublicPropertiesByDefault)
					return null;
			}

			if (!pi.CanWrite)
			{
				if (clpAttr != null)
					this.Errors.Add(pi.Name + " : A read-only property cannot be a command-line " +
									"param, and therefore cannot have a CmdLineArgAttribute.");
				return null;
			}

			CmdLineParam p = PropertyParam.Create(this, clpAttr, pi);

			//if (p != null)
			//{
			//    if (p.Arity != Arity.Multiple)
			//    {
			//        p.ValueSetter = value => { pi.SetValue(this.ParamsObject, value, null); };
			//        p.ValueGetter = ( ) => { return ErrorUtils.ValueToString(pi.GetValue(this.ParamsObject, null)); };
			//    }
			//    else
			//    {
			//        MethodInfo adder = pi.PropertyType.GetMethod("Add");
			//        Assert.NotNull(adder, pi.Name + " : Can't find 'Add' method");

			//        p.ValueSetter = value =>
			//        {
			//            object target = pi.GetValue(this.ParamsObject, null);
			//            if (target == null)
			//                throw new Exception("can't add an item to the collection " +
			//                                    this.ParamsObjType.FullName + "." + pi.Name +
			//                                    " because its value is null");
			//            adder.Invoke(target, new object[] { value });
			//        };
			//    }
			//}

			return p;
		}

		//-----------------------------------------------------------------------------
		// Creates a command-line parameter object that puts its value into a field.
		// Returns null if a parameter wasn't created, either because of an error 
		// (which will have been added to this.Errors, or because no parameter needs
		// to be created for the given field.

		private CmdLineParam CreateParam (FieldInfo fi, CmdLineArgAttribute clpAttr)
		{
			if (clpAttr == null)
				clpAttr = fi.GetCustomAttribute<CmdLineArgAttribute>(false);

			if (clpAttr == null)
			{
				if (!fi.IsPublic)
					return null;
				if (!this.IncludePublicFieldsByDefault)
					return null;
			}

			if (fi.Attributes.HasFlag(FieldAttributes.InitOnly))
			{
				if (clpAttr != null)
					this.Errors.Add(fi.Name + " : A read-only field cannot be a command-line " +
									"param, and therefore cannot have a CmdLineArgAttribute.");
				return null;
			}

			CmdLineParam p = FieldParam.Create(this, clpAttr, fi);

			//if (p != null)
			//{
			//    if (p.Arity != Arity.Multiple)
			//        p.ValueSetter = value => { fi.SetValue(this.ParamsObject, value); };
			//    else
			//    {
			//        MethodInfo adder = fi.FieldType.GetMethod("Add");
			//        Assert.NotNull(adder, fi.Name + " : Can't find 'Add' method");

			//        p.ValueSetter = value => 
			//        {
			//            object target = fi.GetValue(this.ParamsObject);
			//            if (target == null)
			//                throw new Exception("can't add an item to the collection " +
			//                                    this.ParamsObjType.FullName + "." + fi.Name +
			//                                    " because its value is null");
			//            adder.Invoke(target, new object[] { value });
			//        };
			//    }
			//}

			return p;
		}

		//-----------------------------------------------------------------------------
		// Adds the given CmdLineParam to this group or to the CmdLineParser directly,
		// as appropriate.

		internal void Add (CmdLineParam clp)
		{
			Require.NotNull(clp);
			if (this.AllParams != null)
				this.AllParams.Add(clp.Name, clp);
			else this.CmdLineParser.AddParam(clp);
		}

		//-----------------------------------------------------------------------------
		// Adds the given CmdLineParam to this group or to the CmdLineParser directly,
		// and to the given list, as appropriate.

		private void Add (CmdLineParam clp, List<CmdLineParam> positionalParams)
		{
			// in case CreateParam returned null
			if (clp == null)
				return;

			if (clp.CanBePositional)
			{
				Require.Condition(string.IsNullOrEmpty(this.GroupName), 
							"named groups must not contain positional parameters");
				positionalParams.Add(clp);
			}

			if (this.AllParams != null)
				this.AllParams.Add(clp.Name, clp);
			else this.CmdLineParser.AddParam(clp);
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Adds to the given ListString the full names of any required parameters
		/// that weren't found.
		/// </summary>

		internal void CheckRequiredArgs (ListString missing)
		{
			foreach (CmdLineParam p in this.AllParams.Values)
				if (p.IsRequired && !p.Found)
					missing.Add(this.GroupName + "." + p.Name);
		}

		//-----------------------------------------------------------------------------
	}
}
